package org.foreningsgatan.genetics;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Random;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.LinkedBlockingQueue;

import org.foreningsgatan.genetics.grid.ProgramsWithEnvironment;
import org.foreningsgatan.genetics.grid.SimulationTask;
import org.foreningsgatan.genetics.grid.SimulationTask2;
import org.gridgain.grid.Grid;
import org.gridgain.grid.GridException;
import org.gridgain.grid.GridFactory;
import org.gridgain.grid.GridTaskFuture;

public class Generation<T extends Serializable, S extends Serializable> {
	private SimulationEnvironment<T, S> environment;
	private int targetPopulation;
	protected List<Program<T, S>> programs;
	private NodeFactory<T, S> nodeFactory;
	private Random random;

	/**
	 * 
	 * @param progress
	 * @param total
	 */
	private void logProgress(int progress, int total) {
		int p = (progress * 60) / total;
		
		StringBuffer b = new StringBuffer();
		for (int i = 0; i < 60; i++) {
			b.append(p > i ? "#" : ".");
		}
		
		System.out.println(
				String.format("[%5.1f%%] %s %d / %d", (double) progress * 100 / total, 
						b.toString(), progress, total));
	}
	
	/**
	 * 
	 * @param threads
	 * @throws InterruptedException
	 * @throws BrokenBarrierException
	 */
	private void generateRandomPopulation(int threads) throws InterruptedException, BrokenBarrierException {
		System.out.println("generateRandomPopulation");
		programs.clear();
		
		CyclicBarrier barrier = new CyclicBarrier(threads + 2);
		
		new Thread(new ProgressRunnable(programs, targetPopulation, barrier)).start();
		for (int i = 0; i < threads; i++) {
			new Thread(new RandomProgramRunnable(programs, barrier)).start();
		}
		
		barrier.await();
	}
	
	/**
	 * 
	 * @param threads
	 * @throws InterruptedException
	 * @throws BrokenBarrierException
	 * @throws GridException 
	 */
	private void simulatePopulation(int threads) throws InterruptedException, BrokenBarrierException, GridException {
		System.out.println("simulatePopulation");
		Collection<Program<T, S>> simulationQueue = 
			new LinkedBlockingQueue<Program<T,S>>(programs);
		programs.clear();
		
		System.setProperty("GRIDGAIN_HOME", "/Users/jan/Sdk/grid/gridgain-2.1.0");
		GridFactory.start("/Users/jan/Sdk/grid/gridgain-2.1.0/config/jms/activemq/spring-activemq.xml");
        try {
            Grid grid = GridFactory.getGrid();
            GridTaskFuture<Collection> future = grid.execute(SimulationTask2.class, new ProgramsWithEnvironment(simulationQueue, environment));
            programs = (List<Program<T, S>>) future.get();
        }
        finally {
            GridFactory.stop(true);
        }

		
		
//		while (simulationQueue.isEmpty() == false) {
//			Program<T, S> program = simulationQueue.poll();
//			program.run(environment.cloneAndReset());
//			programs.add(program);
//		}
		
//		CyclicBarrier barrier = new CyclicBarrier(threads + 2);
//		
//		new Thread(new ProgressRunnable(programs, targetPopulation, barrier)).start();
//		for (int i = 0; i < threads; i++) {
//			new Thread(new SimulationRunnable(environment, simulationQueue, barrier)).start();
//		}
//		
//		barrier.await();
	}
	
	private int getFitness(int index, int size) {
		int score = size - index;
		
		double factor = Math.pow((double) score / size, 2);
		
		return (int) Math.max(factor * 20, 1);
	}
	
	private Node<T, S> breed(Node<T, S> n1, Node<T, S> n2) {
		Node<T, S> primary, secondary;
		
		if (random.nextBoolean()) {
			primary = n1.clone();
			secondary = n2.clone();
		} else {
			primary = n2.clone();
			secondary = n1.clone();
		}
		
		if (primary instanceof MultipleNode && secondary instanceof MultipleNode) {
			MultipleNode<T, S> mPrimary = (MultipleNode<T, S>) primary;
			MultipleNode<T, S> mSecondary = (MultipleNode<T, S>) secondary;
			
			mPrimary.leftNode = breed(mPrimary.leftNode, mSecondary.leftNode);
			mPrimary.rightNode = breed(mPrimary.rightNode, mSecondary.rightNode);
		}
		
		if (primary instanceof MultipleNode) {
			MultipleNode<T, S> mPrimary = (MultipleNode<T, S>) primary;
			
			if (random.nextFloat() < 0.25) {
				Node<T, S> temp = mPrimary.leftNode;
				mPrimary.leftNode = mPrimary.rightNode;
				mPrimary.rightNode = temp;
			}
		}
		
		if (primary instanceof DecisionNode) {
			DecisionNode<T, S> dPrimary = (DecisionNode<T, S>) primary;
			
			if (random.nextFloat() < 0.25) {
				dPrimary.limit = random.nextFloat() * 2.0f - 1.0f;
			}
			
			if (random.nextFloat() < 0.10) {
				dPrimary.dataSource = nodeFactory.randomDataSource();
			}
		}
		
		if (primary instanceof ActionNode) {
			ActionNode<T, S> aPrimary = (ActionNode<T, S>) primary;
			
			if (random.nextFloat() < 0.10) {
				aPrimary.action = nodeFactory.randomAction();
			}
		}
		
		return primary;
	}
	
	private Program<T, S> breed(Program<T, S> p1, Program<T, S> p2) {
		Node<T, S> rootNode = breed(p1.rootNode, p2.rootNode); 
		
		return new Program<T, S>(rootNode);
	}
	
	protected void breedFrom(Generation<T, S> parent, int threads) throws InterruptedException, BrokenBarrierException {
		// Rate by fitness
		
		List<Program<T, S>> fitness = new ArrayList<Program<T, S>>();
		
		int i = 0;
		for (Program<T, S> p: parent.programs) {
			int counts = getFitness(i, parent.programs.size());
			
			for (int j = 0; j < counts; j++) {
				fitness.add(p);
			}
		}
		
		CyclicBarrier barrier = new CyclicBarrier(2);
		Runnable r = new ProgressRunnable(programs, targetPopulation, barrier);
		
		System.out.println("Breed Population");
		new Thread(r).start();
		
		// Add Survivors
		
		i = 0;
		while (programs.size() < 0.1 * targetPopulation) {
			Program<T, S> survivor = parent.programs.get(i);
			
			if ((survivor != null) && (survivor.age < 5)) {
				programs.add(survivor);
			}
		}
		
		// Breed Children
		
		while (programs.size() < targetPopulation) {
			Program<T, S> parent1 = 
				fitness.get(random.nextInt(fitness.size()));
			Program<T, S> parent2 = 
				fitness.get(random.nextInt(fitness.size()));
			
			programs.add(breed(parent1, parent2));
		}
		
		barrier.await();
		
		try {
			simulatePopulation(threads);
		} catch (GridException e) {
			throw new RuntimeException(e);
		}
		Collections.sort(programs, new ProgramComparator());
	}

	public Generation(int targetPopulation,
			SimulationEnvironment<T, S> environment) {
		this.targetPopulation = targetPopulation;
		this.environment = environment;
		this.programs = new ArrayList<Program<T, S>>();
		this.random = new Random();
	}
	
	public Generation(int targetPopulation,
			SimulationEnvironment<T, S> environment,
			T[] dataSources, S[] actions) {
		this(targetPopulation, environment);
		this.nodeFactory = new NodeFactory<T, S>(dataSources, actions);
	}
	
	public Generation(int targetPopulation,
			SimulationEnvironment<T, S> environment,
			NodeFactory<T, S> nodeFactory) {
		this(targetPopulation, environment);
		this.nodeFactory = nodeFactory;
	}
	
	/**
	 * 
	 * @param threads
	 * @throws Exception
	 */
	public void generateAndSimulateRandomPopulation(int threads) throws Exception {
		generateRandomPopulation(threads);
		simulatePopulation(threads);
		Collections.sort(programs, new ProgramComparator());
	}
	
	public Program<T, S> getFittest() {
		return programs.get(0);
	}

	public Generation<T, S> next(int threads) throws InterruptedException, BrokenBarrierException {
		Generation<T, S> nextGen = new Generation<T, S>(targetPopulation,
				environment, nodeFactory);
		
		nextGen.breedFrom(this, threads);

		return nextGen;
	}

	class RandomProgramRunnable implements Runnable {
		private List<Program<T, S>> programs;
		private CyclicBarrier barrier;
		
		private void generateProgram() {
			Program<T, S> program = new Program<T, S>(nodeFactory.newRootNode());
			
			synchronized (programs) {
				if (programs.size() < targetPopulation) {
					programs.add(program);
				}
			}
		}
		
		public RandomProgramRunnable(List<Program<T, S>> programs, CyclicBarrier barrier) {
			this.programs = programs;
			this.barrier = barrier;
		}

		public void run() {
			try {
				while (programs.size() < targetPopulation) {
					generateProgram();
				}
				
				barrier.await();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}

	class SimulationRunnable implements Runnable {
		private SimulationEnvironment<T, S> environment;
		private BlockingQueue<Program<T, S>> simulationQueue;
		private CyclicBarrier barrier;
		
		private void simulateProgram() {
			Program<T, S> program = simulationQueue.poll();
			
			if (program != null) {
				program.run(environment);
				
				synchronized (programs) {
					programs.add(program);
				}
			}
		}

		public SimulationRunnable(SimulationEnvironment<T, S> environment, 
				BlockingQueue<Program<T, S>> simulationQueue, CyclicBarrier barrier) {
			this.environment = environment;
			this.simulationQueue = simulationQueue;
			this.barrier = barrier;
		}
		
		public void run() {
			try {
				while (!simulationQueue.isEmpty()) {
					simulateProgram();
				}
				
				barrier.await();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}

	class ProgressRunnable implements Runnable {
		private Collection<?> collection;
		private int targetSize;
		private int currentSize;
		private CyclicBarrier barrier;
		
		private void displayProgress() {
			synchronized (collection) {
				if (collection.size() != currentSize) {
					logProgress(collection.size(), targetSize);
					currentSize = collection.size();
				}
			}
		}
		
		public ProgressRunnable(Collection<?> collection, int targetSize, CyclicBarrier barrier) {
			this.collection = collection;
			this.targetSize = targetSize;
			this.barrier = barrier;
			this.currentSize = 0;
		}

		public void run() {
			try {
				int nextSize = 0;
				
				while (collection.size() != targetSize) {
					if (collection.size() > nextSize) {
						displayProgress();
						nextSize += targetSize / 10;
					}
					
					Thread.sleep(50);
				}
				
				logProgress(targetSize, targetSize);
				
				barrier.await();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
	
	class ProgramComparator implements Comparator<Program<T, S>> {
		@Override
		public int compare(Program<T, S> o1, Program<T, S> o2) {
			return environment.compare(o1.resultSet, o2.resultSet);
		}
		
	}
}
